##
#   Project: gmediacatalog - Catalog your media to find them later
#    Author: Fabio Castelli <muflone@vbsimple.net>
# Copyright: 2010 Fabio Castelli
#   License: GPL-2+
#  This program is free software; you can redistribute it and/or modify it
#  under the terms of the GNU General Public License as published by the Free
#  Software Foundation; either version 2 of the License, or (at your option)
#  any later version.
# 
#  This program is distributed in the hope that it will be useful, but WITHOUT
#  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
#  more details.
# 
# On Debian GNU/Linux systems, the full text of the GNU General Public License
# can be found in the file /usr/share/common-licenses/GPL-2.
##

import gc
import subprocess
import threading
import signal
from os import getpid
from time import strftime, localtime

import gtk
import gobject
from gio import content_type_get_description

import handlepaths
import preferences
import plugins
from constants import *
from macros import *
from get_file_size import get_file_size
from get_file_modes import get_file_modes
from get_file_icon import get_icon_from_names, get_stock_icon
from volumeloader import VolumeLoader
from aboutdialog import show_about_dialog
from yesnodialog import show_yesno_dialog
from position_menu_under_widget import position_menu_under_widget

COL_NAME, COL_ICON, COL_PATHID, COL_TYPE, COL_URI, COL_VOLUMEID, \
COL_COMPLETED = range(7)

class GMediaCatalog(object):
  def __init__(self, connection):
    # Start threading support
    gobject.threads_init()
    self.connection = connection
    self.scanFolderProcess = None
    self.locks = threading.Lock()
    self.threadsRefresh = []

    # Load user interface
    builder = gtk.Builder()
    builder.add_from_file(handlepaths.get_path('ui', 'main.glade'))
    builder.connect_signals({
      'on_actionAbout_activate': self.on_actionAbout_activate,
      'on_actionAdd_activate': self.on_actionAdd_activate,
      'on_actionRefresh_activate': self.on_actionRefresh_activate,
      'on_actionStop_activate': self.on_actionStop_activate,
      'on_actionRemove_activate': self.on_actionRemove_activate,
      'on_winMain_delete_event': self.on_winMain_delete_event,
      'on_btnColumns2_clicked': self.on_btnColumns2_clicked,
      'on_columnsmenuitem_toggled': self.on_columnsmenuitem_toggled
    })
    gw = builder.get_object

    self.winMain = gw('winMain')
    self.tvwVolumes = gw('tvwVolumes')
    self.treeVolumes = gw('treeVolumes')
    self.tvwFiles = gw('tvwFiles')
    self.listFiles = gw('listFiles')
    self.actionAdd = gw('actionAdd')
    self.actionRemove = gw('actionRemove')
    self.actionHidden = gw('actionHidden')
    self.actionRefresh = gw('actionRefresh')
    self.actionStop = gw('actionStop')
    self.actionAbout = gw('actionAbout')
    
    tlbTools = gw('tlbTools')
    imgColumns = gw('imgColumns')
    arrowColumns = gw('arrowColumns')
    lblColumns = gw('lblColumns')
    tlbTools_style = tlbTools.get_style()
    # Set fake toolbar style for manually created columns button
    if tlbTools_style == gtk.TOOLBAR_TEXT:
      set_visible(imgColumns, False)
      set_visible(arrowColumns, False)
    elif tlbTools_style in (gtk.TOOLBAR_ICONS, gtk.TOOLBAR_BOTH_HORIZ):
      set_visible(lblColumns, False)
    
    self.btnColumns = gw('btnColumns')
    self.btnRefresh = gw('btnRefresh')
    self.menuColumns = gw('menuColumns')

    self.tvwVolumes.get_selection().connect('changed', 
      self.on_tvwVolumes_selection_changed)

    # Assign menu items to columns
    menuitems_for_columns = {
      'menuitemName': 'colFileName',
      'menuitemSize': 'colFileSize',
      'menuitemType': 'colFileContentType',
      'menuitemModified': 'colFileModified',
      'menuitemChanged': 'colFileChanged',
      'menuitemAccess': 'colFileAccess',
      'menuitemModes': 'colFileModes',
      'menuitemNumericModes': 'colFileNumericModes',
      'menuitemUser': 'colFileUser',
      'menuitemGroup': 'colFileGroup',
      'menuitemHidden': 'colFileHidden'
    }
    for k, v in menuitems_for_columns.iteritems():
      assert gw(v), (k, v)
      gw(k).connect('toggled', self.on_columnsmenuitem_toggled, gw(v))

    # Set custom functions for cell renderers
    column = gw('colFileContentType')
    cell_renderer = gw('crFileContentType')
    column.set_cell_data_func(cell_renderer, self._format_content_type, 7)
    
    column = gw('colFileModified')
    cell_renderer = gw('crFileModified')
    column.set_cell_data_func(cell_renderer, self._format_date, 10)

    column = gw('colFileChanged')
    cell_renderer = gw('crFileChanged')
    column.set_cell_data_func(cell_renderer, self._format_date, 11)

    column = gw('colFileAccess')
    cell_renderer = gw('crFileAccess')
    column.set_cell_data_func(cell_renderer, self._format_date, 12)

    # Set signals handler for external process
    signal.signal(signal.SIGUSR1, self.signals_handler)
    signal.signal(signal.SIGUSR2, self.signals_handler)
    
    # Set window
    self.winMain.set_icon_from_file(handlepaths.get_app_logo())
    self.winMain.set_default_size(600, 300)
    self.winMain.set_title(APP_TITLE)

  def run(self):
    # Load volumes
    self.on_actionRefresh_activate(None)
    # Show user interface
    self.winMain.show()
    gtk.gdk.threads_enter()
    gtk.main()
    gtk.gdk.threads_leave()

  def on_actionAbout_activate(self, widget, data=None):
    "Show the about dialog"
    show_about_dialog()

  def on_actionAdd_activate(self, widget, data=None):
    "Add new volume dialog"
    self.actionRefresh.set_sensitive(False)
    self.actionAdd.set_sensitive(False)
    self.scanFolderProcess = subprocess.Popen([
      SCAN_VOLUME_PROCESS,
      str(getpid())
    ])

  def on_actionRefresh_activate(self, widget, data=None):
    "Reload volumes"
    gc.collect()
    # Acquire lock to avoid race conditions
    self.locks.acquire()
    # No threads are running, perform a refresh
    if not self.threadsRefresh:
      # Change refresh button to stop
      self.set_refresh_buttons(False)
      gtk_process_events()
      # Disconnect model from widget to make it REALLY faster
      self.tvwVolumes.set_model(None)
      self.tvwFiles.set_model(None)
      # Clear previous models
      self.listFiles.clear()
      self.treeVolumes.clear()
      # Read volumes
      curVolumes = self.connection.select_volumes()
      for rowVolume in curVolumes:
        # Cycle volumes
        threadRefresh = VolumeLoader(
          self.connection, 
          rowVolume['volume_id'], 
          rowVolume['volume_root'].replace('%20', ' '),
          rowVolume['volume_name'],
          self.treeVolumes,
          self.thread_completed
        )
        self.threadsRefresh.append(threadRefresh)
        threadRefresh.start()
        #threadRefresh.join()
      curVolumes.close()
      # No threads are running, restore refresh button
      if not self.threadsRefresh:
        self.set_refresh_buttons(True)
    # Release lock
    self.locks.release()
  
  def on_actionStop_activate(self, widget, data=None):
    "Cancel running threads"
    # Threads still running, stop them all
    for thread in self.threadsRefresh:
      thread.cancel()
    
  def on_actionRemove_activate(self, widget, data=None):
    "Remove selected volume"
    def remove_children(parent):
      children = []
      # Find first children
      iter = self.treeVolumes.iter_children(parent)
      while iter:
        # Save new child
        children.append(iter)
        # Remove further children
        if self.treeVolumes.iter_has_child(iter):
          remove_children(iter)
        # Find next child
        iter = self.treeVolumes.iter_next(iter)
      # Remove all children
      for iter in children:
        print self.treeVolumes[iter][COL_URI]
        self.treeVolumes.remove(iter)
      # Clear children list
      children = None

    iter = self.tvwVolumes.get_selection().get_selected()[1]
    if iter:
      if show_yesno_dialog('Are you sure to remove the selected volume?'
        ) == gtk.RESPONSE_YES:
        # Notify volume delete to plugins
        plugins.signal_proxy('on_delete_volume', 
          argc=1, args=self.treeVolumes[iter][COL_VOLUMEID])
        # Delete data from database
        self.connection.delete_volume(self.treeVolumes[iter][COL_VOLUMEID])
        # Clear every children if DEEP_REMOVE is enabled
        if preferences.DEEP_REMOVE:
          remove_children(iter)
        self.treeVolumes.remove(iter)
        self.listFiles.clear()

  def on_winMain_delete_event(self, widget, signal, data=None):
    "Main window closing"
    plugins.signal_proxy('on_closing')
    self.winMain.hide()
    # Cancel running threads"
    gtk_process_events()
    self.tvwVolumes.set_model(None)
    self.tvwFiles.set_model(None)
    # Cancel running threads
    self.on_actionStop_activate(widget, data)
    # Is the add volume process still running?
    if self.scanFolderProcess is not None:
      if self.scanFolderProcess.poll() is None:
        # Close it forcefully
        print self.scanFolderProcess, dir(self.scanFolderProcess)
        self.scanFolderProcess.terminate()
      self.scanFolderProcess = None
    plugins.signal_proxy('on_closed')
    gtk.main_quit()

  def thread_completed(self, thread):
    "A loader thread has completed"
    # Acquire lock to avoid race conditions
    self.locks.acquire()
    if thread in self.threadsRefresh:
      self.threadsRefresh.remove(thread)
      # When SCAN_METHOD_SAFE is disabled the models will be attached
      # after the first completed thread
      if not preferences.SCAN_METHOD_SAFE:
        self.tvwVolumes.set_model(self.treeVolumes)
        self.tvwFiles.set_model(self.listFiles)
    else:
      print 'thread not in list', thread
    # No more remaining threads, change stop button to refresh
    if not self.threadsRefresh:
      self.set_refresh_buttons(True)
      gtk_process_events()
      # When SCAN_METHOD_SAFE is enabled the models will be attached
      # only after all the threads are completed
      if preferences.SCAN_METHOD_SAFE:
        self.tvwVolumes.set_model(self.treeVolumes)
        self.tvwFiles.set_model(self.listFiles)
    # Release lock
    self.locks.release()
    gc.collect()

  def on_tvwVolumes_selection_changed(self, selection):
    "Load files from the selected volume/folder"
    iter = selection.get_selected()[1]
    # Enable remove button
    self.actionRemove.set_sensitive(bool(
      iter and (self.treeVolumes[iter][COL_TYPE] == ITEM_VOLUME)))
    # An item was selected? Is the external process not running?
    if iter and not self.is_scanFolderProcess_running():
      # Clear files list
      self.tvwFiles.set_model(None)
      self.listFiles.clear()
      # Is the volume fully loaded?
      if self.treeVolumes[get_iter_root(self.treeVolumes, iter)][COL_COMPLETED]:
        # Load files with current folder
        curFile = self.connection.select_files(self.treeVolumes[iter][COL_PATHID])
        # Load generic icons
        icon_file = get_stock_icon('gtk-file')
        icon_folder = get_stock_icon('gtk-directory')
        # Cycle files
        for rowFile in curFile:
          # Show unhidden and hidden files if requested
          if (rowFile['file_flags'] and 1) == 0 or \
            self.actionHidden.get_active():
            # Get file size
            fileSize = get_file_size(
              self.connection,                              # Backend Connection
              rowFile['file_type'],                         # File type
              rowFile['file_target'],                       # Symlink target
              rowFile['file_size'],                         # File size in bytes
              self.treeVolumes[iter][COL_VOLUMEID],
              '%s/%s' % (self.treeVolumes[iter][COL_URI], rowFile['file_name'])
            )
            # Find icon
            if rowFile['file_type'] == ITEM_FILE:
              icon = get_icon_from_names(str(rowFile['file_icon']).split(';'))
              if not icon:
                icon = icon_file
            else:
              icon = icon_folder

            # Add file
            self.listFiles.append([
              icon,                                         # Icon
              rowFile['file_name'],                         # Name
              rowFile['file_user'],                         # User
              rowFile['file_group'],                        # Group
              bool(rowFile['file_flags'] and 1),            # Hidden
              get_file_modes(rowFile['file_mode']),         # Modes
              oct(rowFile['file_mode'])[1:],                # Octal modes
              rowFile['file_content_type'],                 # Content Type
              fileSize,                                     # File size
              rowFile['file_size'],                         # File size bytes
              rowFile['file_modification'],                 # Modification date
              rowFile['file_changed'],                      # Change date
              rowFile['file_access'],                       # Access date
              rowFile['file_description']                   # Description
            ])
        curFile.close()
      self.tvwFiles.set_model(self.listFiles)

  def signals_handler(self, signalnum, handler):
    "Listen for incoming signals"
    if signalnum == signal.SIGUSR1:
      # Enable buttons again after closing the add volume process
      self.actionRefresh.set_sensitive(True)
      self.actionAdd.set_sensitive(True)
      self.scanFolderProcess = None

  def _format_date(self, column, cell, model, iter, column_index):
    value = model.get_value(iter, column_index)
    cell.set_property('text', strftime('%c', localtime(value)))

  def _format_content_type(self, column, cell, model, iter, column_index):
    value = model.get_value(iter, column_index)
    cell.set_property('text', content_type_get_description(value))

  def on_columnsmenuitem_toggled(self, widget, data=None):
    data.set_visible(widget.get_active())

  def on_btnColumns2_clicked(self, widget, data=None):
    "Popup menu under the button"
    self.menuColumns.popup(None, None, position_menu_under_widget,
      gtk.gdk.BUTTON_PRESS, gtk.get_current_event_time(), self.btnColumns)

  def is_scanFolderProcess_running(self):
    "Check if the scanFolder process is running"
    return self.scanFolderProcess and (
      self.scanFolderProcess.poll() is not None) or False

  def set_refresh_buttons(self, status):
    self.actionRefresh.set_sensitive(status)
    self.actionStop.set_sensitive(not status)
    if preferences.HIDE_REFRESH_STOP_BUTTON:
      self.actionRefresh.set_visible(status)
      self.actionStop.set_visible(not status)
